﻿<script setup lang="ts" name="CropperImage">
import type { CSSProperties } from 'vue'
import { PropType } from 'vue'
import Cropper from 'cropperjs'
import 'cropperjs/dist/cropper.css'
import { useDebounceFn } from '@vueuse/shared'
type Options = Cropper.Options;

const defaultOptions: Options = {
  aspectRatio: 1,
  zoomable: true,
  zoomOnTouch: true,
  zoomOnWheel: true,
  cropBoxMovable: true,
  cropBoxResizable: true,
  toggleDragModeOnDblclick: true,
  autoCrop: true,
  background: true,
  highlight: true,
  center: true,
  responsive: true,
  restore: true,
  checkCrossOrigin: true,
  checkOrientation: true,
  scalable: true,
  modal: true,
  guides: true,
  movable: true,
  rotatable: true
}
const props = defineProps({
  src: { type: String, required: true },
  alt: { type: String },
  circled: { type: Boolean as any, default: false },
  realTimePreview: { type: Boolean, default: true },
  height: { type: [String, Number], default: '360px' },
  crossorigin: {
    type: String as PropType<'' | 'anonymous' | 'use-credentials' | undefined>,
    default: undefined
  },
  imageStyle: { type: Object as PropType<CSSProperties>, default: () => ({}) },
  options: { type: Object as PropType<Options>, default: () => ({}) }
})

const imgElRef = ref<ElRef<HTMLImageElement>>()
const cropper = ref<Nullable<Cropper>>()
const isReady = ref(false)

const getImageStyle = computed((): CSSProperties => {
  return {
    height: props.height,
    maxWidth: '100%',
    ...props.imageStyle
  }
})

const getWrapperStyle = computed((): CSSProperties => {
  return { height: `${props.height}`.replace(/px/, '') + 'px' }
})

const emit = defineEmits(['cropend', 'ready', 'cropendError'])

const realTimeCroppered = () => {
  props.realTimePreview && croppered()
}
const debounceRealTimeCroppered = useDebounceFn(realTimeCroppered, 80)

const croppered = () => {
  if (!cropper.value) {
    return
  }
  const imgInfo = cropper.value.getData()
  const canvas = props.circled ? getRoundedCanvas() : cropper.value.getCroppedCanvas()
  canvas.toBlob((blob) => {
    if (!blob) {
      return
    }
    const fileReader: FileReader = new FileReader()
    fileReader.readAsDataURL(blob)
    fileReader.onloadend = (e) => {
      emit('cropend', {
        imgBase64: e.target?.result ?? '',
        imgInfo
      })
    }
    fileReader.onerror = () => {
      emit('cropendError')
    }
  }, 'image/png')
}

const getRoundedCanvas = () => {
  const sourceCanvas = cropper.value!.getCroppedCanvas()
  const canvas = document.createElement('canvas')
  const context = canvas.getContext('2d')!
  const width = sourceCanvas.width
  const height = sourceCanvas.height
  canvas.width = width
  canvas.height = height
  context.imageSmoothingEnabled = true
  context.drawImage(sourceCanvas, 0, 0, width, height)
  context.globalCompositeOperation = 'destination-in'
  context.beginPath()
  context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true)
  context.fill()
  return canvas
}

const init = async () => {
  const imgEl = unref(imgElRef)
  if (!imgEl) {
    return
  }
  cropper.value = new Cropper(imgEl, {
    ...defaultOptions,
    ready: () => {
      isReady.value = true
      realTimeCroppered()
      emit('ready', cropper.value)
    },
    crop() {
      debounceRealTimeCroppered()
    },
    zoom() {
      debounceRealTimeCroppered()
    },
    cropmove() {
      debounceRealTimeCroppered()
    },
    ...props.options
  })
}

onMounted(init)

onUnmounted(() => {
  cropper.value?.destroy()
})
</script>

<template>
  <div :class="['cropper-image', props.circled ? 'cropper-image--circled' : '']" :style="getWrapperStyle">
    <img v-show="isReady" ref="imgElRef" :src="src" :alt="alt" :crossorigin="crossorigin" :style="getImageStyle" />
  </div>
</template>

<style scoped lang="scss">
.cropper-image.cropper-image--circled{
  :deep(.cropper-view-box),:deep(.cropper-face) {
    border-radius: 50%;
  }
}
</style>
<style>
.cropper-view-box {
  outline: 1px solid var(--el-color-primary) !important;
  /* stylelint-disable-next-line color-function-notation */
  outline-color: rgba(var(--el-color-primary-rgb), 0.75) !important;
}
</style>
